blessed 是基于 Node.js 的、功能强大且高级的终端界面创建库(Cursed-like library)。它的核心定位是让开发者能够利用 JavaScript,像构建网页应用一样,轻松地在命令行终端中创建复杂、美观、交互丰富的图形用户界面(TUI)
blessed 通过抽象底层的终端控制序列(如 terminfo 和 termcap),提供一套面向对象的 API,开发者可以通过声明式的方式定义和组织各种 UI 组件,而无需关心具体的终端类型和控制代码
pnpm install blessedblessed 的应用场景非常广泛,几乎涵盖了所有需要在终端中提供丰富交互体验的场合:
blessed 经常被拿来与 ncurses(及其 Node.js 包装)等传统 TUI 库进行比较
| 特性 | blessed | ncurses |
|---|---|---|
| 抽象层次 | 高层抽象。提供了面向对象的组件模型,更接近现代 GUI 框架 | 底层库。提供更基础的窗口和字符控制功能,API 相对原始 |
| 开发效率 | 高。声明式 API 和丰富的内置组件可以快速构建复杂界面 | 较低。需要手动管理窗口、屏幕刷新和事件循环,开发周期较长 |
| 布局系统 | 现代化。支持类似 CSS 的布局和样式配置,支持百分比、相对定位等 | 基础。通常需要手动计算坐标和尺寸 |
| 事件模型 | 强大。内置了完善的事件系统,支持鼠标、键盘、窗口事件 | 基础。事件处理相对简单,需要开发者自行实现复杂的事件逻辑 |
| 社区生态 | 活跃。拥有丰富的第三方插件和组件,如 blessed-contrib | 非常成熟。作为事实上的标准,拥有庞大的用户基础和长期的技术积累 |
| 性能 | 良好。针对 Node.js 环境进行了优化,但对于极端性能场景可能不如 C/C++ 库 | 极高。作为 C 语言库,性能非常出色 |
总而言之,blessed 以牺牲部分底层控制和极致性能为代价,换来了极高的开发效率和更现代化的开发体验,特别适合需要快速迭代和构建复杂界面的 Node.js 项目
blessed 内置丰富的 UI 组件(官方称为 Element 或 Node),基本可以满足绝大部分 TUI 应用的需求。所有组件都继承自基类 Element 和 Node
基础容器
Element: 所有可见组件的基类,提供基础的样式和布局能力Box: 一个简单的矩形容器,可用于布局和内容承载,类似 HTML 中的 <div>Screen: 根节点,代表整个终端屏幕,是所有其他组件的顶级容器文本与内容
Text: 显示纯文本内容Line: 绘制水平或垂直的线条BigText: 使用 ASCII 字符绘制大字体文本Log: 可滚动的日志输出组件,适合显示实时日志Markdown: 解析并渲染 Markdown 格式的文本交互式组件
List: 可选择的列表,支持键盘导航Listbar: 水平菜单栏,类似传统 GUI 应用的顶部菜单File-Manager: 一个简单的文件浏览器组件Form: 表单容器,用于管理输入型组件Input, Textbox, Textarea: 不同形式的文本输入框Button: 可点击的按钮Checkbox: 复选框RadioSet & RadioButton: 单选按钮组Prompt: 弹出一个提示框,等待用户输入Question: 弹出一个确认对话框(Yes/No)数据可视化
ProgressBar: 进度条Table: 渲染表格数据ListTable: 结合了 List 和 Table 的功能,即可选择的表格其他
Terminal: 在 blessed 界面中嵌入一个真实的终端会话Image: 显示图片(通过内置的 png/gif 转终端字符渲染器,即 ANSIImage,或借助 w3mimgdisplay 的 OverlayImage)blessed 拥有一个强大的、类似 DOM 的事件系统。每个组件都是一个事件发射器(EventEmitter),可以监听和响应各种事件
键盘事件: screen 对象和可聚焦的组件(如 Input、List)可以监听键盘事件
keypress: 任何按键被按下时触发key [name]: 特定按键被按下时触发,例如 key enteronce('keypress', ...): 监听一次按键事件鼠标事件: 需要在组件上设置 mouse: true(或调用 screen.enableMouse(),绑定 mouse 事件时会自动调用)来启用鼠标支持。注意 Windows 下暂不支持鼠标与 resize 事件
mousedown, mouseup, mousemove: 鼠标按键按下、抬起和移动wheeldown, wheelup: 鼠标滚轮事件click: 组件被点击时触发focus, blur: 组件获得或失去焦点时触发组件生命周期与状态事件:
attach, detach: 组件被添加或移除出渲染树。show, hide: 组件可见性改变。move, resize: 组件位置或尺寸改变。prerender, render: 组件渲染前后触发。事件可以进行冒泡。如果在子组件上触发了一个事件,但没有被处理,它会向上传递到父组件,直到 screen 对象。也可以在任何祖先组件上通过 element [name] 形式的全局事件(如 box.on("element click", (el, mouse) => {}))统一接收所有后代组件的事件。blessed 没有 stopPropagation 这样的 API,如需区分事件来源,在监听器中判断传入的 el 即可。
blessed 的布局和样式系统非常灵活,其设计思想借鉴了 CSS
尺寸与定位:
top, left, right, bottom 可以是数字(字符数)或百分比字符串('50%')width, height 同样支持数字和百分比top, left 等设置为相对于父容器中心的值,例如 left: 'center'top: '50%-5'样式配置 (style 对象):
fg (前景色), bg (背景色)。可以是颜色名称('red')、十六进制值('#ff0000')或 256 色索引bold, underline, blink, inverse (反色)border 对象):
type: 'line' (默认) 或 'bg' (使用背景色填充)ch: 用于绘制边框的字符,默认为 (空格)fg, bg: 边框颜色。style 对象中可以包含 hover (鼠标悬停), focus (获得焦点), selected (列表项被选中) 等伪类的样式const box = blessed.box({
parent: screen,
top: "center",
left: "center",
width: "80%",
height: "90%",
border: {
type: "line",
fg: "cyan"
},
style: {
fg: "white",
bg: "blue",
hover: {
bg: "green"
}
}
})blessed 致力于提供良好的跨平台兼容性,能在绝大多数现代终端模拟器上运行,包括:
xterm、gnome-terminal、 konsole、 terminator 等Terminal.app、iTerm2cmd.exe、 PowerShell、Windows Terminal。在 Windows 上,blessed 内部会使用 windows-ansi 终端类型进行适配(新版本已自动处理,旧版本需手动设置 terminal: "windows-ansi")尽管 blessed 做了很多兼容性工作,但某些高级特性(如图片显示、24 位真彩色)仍然依赖于特定终端的支持
blessed 会自动检测终端支持的颜色数量(2、8、16、256、16777216),并尽可能优雅降级在开发时,如果遇到平台相关的显示问题,通常需要检查终端本身的配置(如字体、编码、TERM 环境变量等)
blessed 的 API 设计是面向对象的,理解其核心类和方法是高效开发的关键
blessed.screen(options)screen 是 blessed 应用的根节点和心脏。它继承自 Box,并负责管理全局状态、事件循环和屏幕渲染
构造函数: blessed.screen(options)
options (Object): 配置对象。
smartCSR (Boolean): 默认 false,建议开启。启用智能光标渲染优化(CSR),可以大幅提升性能。sendFocus (Boolean): 默认 false。在鼠标启用后发送 focus 和 blur 事件(需要终端支持焦点上报协议)。autoPadding (Boolean): 默认 false(官方推荐开启)。自动为带边框和内边距的元素调整子元素定位。dockBorders (Boolean): 默认 false。边框停靠在元素边缘(实验特性)。fullUnicode (Boolean): 默认 false。开启完整的 Unicode 支持,正确处理东亚宽字符。terminal (String): 指定终端类型,覆盖 process.env.TERM。关键方法:
screen.render(): 手动触发一次全屏重绘。blessed 会自动批量处理更新,但有时需要手动调用。screen.destroy(): 销毁 screen 和所有子元素,恢复终端到原始状态。screen.key(keys, listener): 注册全局快捷键。例如 screen.key(['q', 'C-c'], () => process.exit(0));。screen.append(element): 将一个元素添加到 screen 中。screen.remove(element): 从 screen 中移除一个元素。blessed.node(options) (基类)Node 是所有组件的顶层基类,主要负责组件树的层级关系(父子、兄弟节点)
核心属性:
parent: 父节点children: 子节点数组type: 组件类型字符串(如 'box')核心方法:
append(element): 添加一个子节点prepend(element): 在子节点列表的开头添加一个节点remove(element): 移除一个子节点insert(element, index): 在指定索引处插入一个子节点emit(event, ...args): 触发一个事件on(event, listener): 监听一个事件blessed.element(options) (基类)Element 继承自 Node,是所有可见组件的基类。它添加了位置、尺寸、样式和内容等视觉相关的属性和方法
核心属性:
options (Object): 包含所有配置,如 top, left, width, height, style, border, content 等width, height, top, left: 最终计算出的尺寸和位置content (String): 组件显示的文本内容核心方法:
show(): 显示组件hide(): 隐藏组件toggle(): 切换可见性focus(): 使组件获得焦点setContent(text): 设置组件的文本内容setFront(): 将组件置于其兄弟节点的顶层setBack(): 将组件置于其兄弟节点的底层blessed.box(options)Box 是最常用的基础容器组件,继承自 Element。它本身没有太多特殊功能,主要用于布局和作为其他组件的容器
以下是一些在创建组件时最常用的配置参数:
parent (Node): 必需。指定该组件的父节点,通常是 screen 或另一个容器组件top, left, right, bottom (Number | String): 控制位置。可以是数字(行/列),或字符串('center','50%','50%-2')width, height (Number | String): 控制尺寸。规则同上content (String): 要显示的文本内容。可以使用 blessed 的标签语法进行样式设置,例如 'Hello {bold}world{/bold}!'label (String | Object): 在边框上显示的标签。如果是对象,可以设置 { text: 'Label', side: 'left' }border (Object | String): 边框样式。如果是字符串,则指定 type。对象可以包含 type, ch, fg, bgstyle (Object): 样式对象,包含 fg, bg, bold, underline, hover, focus 等hidden (Boolean): true 则初始状态为隐藏scrollable (Boolean): true 使内容可滚动(通过鼠标或按键)keys (Boolean): true 使组件可以接收键盘事件mouse (Boolean): true 使组件可以接收鼠标事件vi (Boolean): true 为可滚动组件启用 vi 风格的导航键(k, j, g, G)tags (Boolean): 默认 false。开启后解析内容中的标签语法(如 {bold}...{/bold})总是指定 parent:
创建任何组件时,都必须明确其 parent。这能确保组件被正确地添加到组件树中并进行渲染。
使用 screen.destroy() 清理:
当你的应用程序退出时,务必调用 screen.destroy()。这会恢复终端到原始模式,移除所有监听器,并销毁所有子组件,防止终端状态混乱。
合理使用 render():
blessed 会在内部对 render 调用进行优化和节流。通常你不需要频繁地手动调用 screen.render()。只有在进行了大量同步更新后,希望立即看到变化时,才需要手动调用。
动态添加和移除组件:
使用 parent.append(child) 和 child.destroy() (或 parent.remove(child)) 来动态管理界面元素。destroy() 会彻底清理组件及其子组件,并移除所有相关的监听器,是比 remove 更彻底的清理方式。
事件监听器管理:
在 destroy 组件时,blessed 会自动移除该组件上通过 .on() 方法添加的监听器。这避免了常见的内存泄漏问题
// 退出逻辑
screen.key(["q", "C-c"], (ch, key) => {
// 在退出前销毁 screen 对象
return screen.destroy()
})理论结合实践是最好的学习方式。以下是一些从简到繁的代码示例
这是一个最简单的 blessed 应用,它会在屏幕中央显示一个带边框的文本框
// hello-world.js
const blessed = require("blessed")
// 创建一个 screen 对象
const screen = blessed.screen({
smartCSR: true,
title: "Hello Blessed!"
})
// 创建一个 box 组件
const box = blessed.box({
parent: screen,
top: "center",
left: "center",
width: "50%",
height: "50%",
content: "Hello {bold}world{/bold}!",
tags: true,
border: {
type: "line"
},
style: {
fg: "white",
bg: "magenta",
border: {
fg: "#f0f0f0"
}
}
})
// 监听 'q' 或 'Ctrl+C' 按键事件,用于退出程序
screen.key(["q", "C-c"], (ch, key) => {
return process.exit(0)
})
// 渲染屏幕
screen.render()创建 list.js
screen.append 来添加fullUnicode: true 这样可以支持中文字符import blessed from "blessed"
const screen = blessed.screen({
fullUnicode: true
})
const data = [
"白夜行",
"解忧杂货店",
"挪威的森林",
"追风筝的人",
"小王子",
"飘",
"麦田里的守望者",
"时间简史",
"人类简史",
"活着为了讲述",
"白夜行",
"百鬼夜行"
]
const list = blessed.list({
width: "50%",
height: "50%",
border: "line",
label: "书籍列表",
align: "left",
right: 0,
bottom: 0,
keys: true,
mouse: true,
style: {
fg: "white",
bg: "default",
selected: {
bg: "blue"
}
},
items: data
})
screen.append(list)
list.select(0)
list.on("select", function (item) {
screen.destroy()
console.log(item.getText())
})
screen.key("C-c", function () {
screen.destroy()
})
list.focus()
screen.render()运行文件:
node list.js创建 form.js
const blessed = require("blessed")
const screen = blessed.screen({
fullUnicode: true
})
const prompt = blessed.prompt({
parent: screen,
border: "line",
height: "shrink",
width: "half",
top: "center",
left: "center",
label: " {blue-fg}登录{/blue-fg} ",
tags: true
})
const msg = blessed.message({
parent: screen,
border: "line",
width: "half",
height: "shrink",
top: "center",
left: "center",
label: " {blue-fg}提示{/blue-fg} ",
tags: true,
hidden: true
})
prompt.input("你的用户名?", "", function (err, username) {
prompt.input("你的密码?", "", function (err, password) {
if (username === "guang" && password === "aaa123") {
msg.display("登录成功!", 1)
} else {
msg.display("用户名或密码错误!", 1)
}
setTimeout(function () {
screen.destroy()
console.log(username, password)
}, 1000)
})
})
screen.key("C-c", function () {
screen.destroy()
})
screen.render()创建 filemanger.js
import blessed from "blessed"
const screen = blessed.screen({
fullUnicode: true
});
const fm = blessed.filemanager({
parent: screen,
border: 'line',
height: 'half',
width: 'half',
top: 'center',
left: 'center',
label: ' {blue-fg}%path{/blue-fg} ',
cwd: process.cwd(),
keys: true,
style: {
selected: {
bg: 'blue'
}
},
scrollbar: {
bg: 'white'
}
});
fm.on('file', (file)=> {
screen.destroy();
console.log(file);
})
screen.key('C-c', function() {
screen.destroy();
});
fm.refresh();
screen.render();
创建 table.js
const blessed = require("blessed")
const screen = blessed.screen({
fullUnicode: true
})
const table = blessed.table({
parent: screen,
width: "80%",
height: "shrink",
top: "center",
left: "center",
data: null,
border: "line",
align: "center",
tags: true,
style: {
border: {
fg: "white"
},
header: {
fg: "blue",
bold: true
},
cell: {
fg: "green"
}
}
})
const data = [
["姓名", "性别", "年龄", "电话号码"],
["东东", "男", "20", "13233334444"],
["光光", "男", "20", "13233332222"],
["小红", "女", "21", "13233335555"],
["小刚", "男", "22", "13233336666"]
]
data[1][0] = "{red-fg}" + data[1][0] + "{/red-fg}"
table.setData(data)
screen.key("C-c", function () {
screen.destroy()
})
screen.render()创建 progressbar.js
import blessed from "blessed"
const screen = blessed.screen({
fullUnicode: true
});
const progressBar = blessed.progressbar({
parent: screen,
top: '50%',
left: '50%',
height: 2,
width: 20,
style: {
bg: 'gray',
bar: {
bg: 'green'
}
}
})
screen.key('C-c', function() {
screen.destroy();
});
let total = 0;
const timer = setInterval(() => {
if(total === 100) {
clearInterval(timer);
}
progressBar.setProgress(total)
screen.render();
total += 2;
}, 100);
screen.render();尽管 blessed 提供了很高的开发效率,但在构建复杂或高刷新率的 TUI 应用时,性能问题仍然值得关注。以下是一些关键的性能优化技巧。
保持 smartCSR: true:
这是最重要的性能开关,但注意它默认是关闭的,创建 screen 时需要显式传入。smartCSR (Smart Cursor and Screen Rendering) 机制会计算出屏幕上真正发生变化的区域,并只更新这部分内容,而不是每次都重绘整个屏幕。这能极大地减少终端的 I/O 操作
减少不必要的 screen.render() 调用:
blessed 会在事件循环的下一个 tick 中自动批量处理更新并调用 render。除非你确实需要立即看到更新,否则应避免手动、高频地调用 screen.render()。例如,在一个循环中更新多个组件时,只需在循环结束后让 blessed 自动渲染一次即可
使用 box 作为静态背景:
如果你的界面有复杂的静态背景(例如,由很多 box 组成的边框或图案),可以考虑将它们组合在一个大的 box 中,并尽量避免重绘它。将动态内容放在这个静态背景的上层
隐藏不可见的元素:
如果一个元素暂时不需要显示,使用 element.hide() 将其隐藏。被隐藏的元素不会参与渲染计算,可以减轻 blessed 的工作量。对于弹出式窗口或标签页界面,这一点尤其重要
避免复杂的文本标签 (tags):
虽然 {bold} 等标签很方便,但 blessed 需要解析这些标签并转换为终端序列。对于需要频繁更新的大段文本,如果其中不包含样式变化,可以考虑设置 tags: false 来关闭标签解析,从而获得微小的性能提升
及时销毁不再使用的组件:
当一个组件(尤其是包含大量子组件或持有大量数据的组件,如 Log 或 List)不再需要时,应该调用 element.destroy() 来销毁它。destroy 方法会:
谨慎处理 Log 组件:
Log 组件默认会缓存所有添加的日志行,以便用户可以回滚查看。如果日志量非常大,这可能会消耗大量内存。可以考虑:
scrollback 选项,限制缓存的行数Log 内容,例如 logger.setContent('')管理外部数据引用:
确保在组件的事件监听器中引用的外部对象能够被垃圾回收。如果一个组件被销毁了,但外部还有一个对象持有对它的引用,或者它的监听器闭包中引用了不会被释放的外部对象,都可能导致内存泄漏
分页和虚拟滚动:
对于需要显示大量数据(成千上万行)的列表或表格,不要一次性将所有数据都添加到组件中。应该实现分页逻辑,或者像虚拟滚动一样,只向组件中添加当前视口内可见的数据项。当用户滚动时,动态地更新组件的内容
延迟加载组件:
对于复杂的、由多个页面或标签组成的 TUI,可以采用延迟加载的策略。只在用户切换到某个页面或标签时,才创建和渲染该页面所需的组件,而不是在启动时就创建所有组件
简化布局计算:
blessed 的百分比和 center 布局非常方便,但每次 render 都需要重新计算。对于一个固定的、网格化的布局,可以考虑在启动时计算好所有组件的绝对位置和尺寸,之后不再改变。这可以减少 blessed 在渲染时的布局计算开销
测试不同终端的性能:
不同的终端模拟器在处理大量数据输出时的性能差异可能很大。例如,iTerm2 和 Alacritty (GPU 加速) 通常比 Terminal.app 或 xterm 在高吞吐量下的表现更好。如果你的应用对性能要求很高,建议在目标用户常用的高性能终端上进行测试和优化
blessed 拥有一个虽然不大但相当活跃的生态系统,提供了许多实用的插件和工具,可以极大地扩展其功能。
blessed 官方并未维护一个正式的插件列表,但其社区贡献了大量高质量的模块。
blessed-contrib: 这是 blessed 生态系统中最著名、最强大的扩展包。它提供一系列用于构建仪表盘(Dashboard)的高级组件,包括:
Grid: 一个网格布局系统,可以轻松地将屏幕划分为多个区域Line Chart: 实时折线图Bar Chart: 柱状图Stacked Bar Chart: 堆叠柱状图Map: 在终端中显示世界地图Donut: 甜甜圈图Gauge: 仪表盘式的计量器Sparkline: 紧凑的迷你折线图LCD Display: 模拟 LCD 数字显示效果blessed-contrib 是构建监控类 TUI 应用的首选工具
blessed-xterm: 允许你在 blessed 应用中嵌入一个功能完整的 xterm 终端。这比 blessed 内置的 Terminal 组件功能更强大,因为它直接使用了 node-pty 和 xterm.js 的部分代码,提供了更真实的终端模拟
blessed-react: 使用 React 的声明式语法和 JSX 来构建 blessed 界面。它将 React 组件的生命周期和状态管理与 blessed 的渲染能力结合起来,使得构建复杂的、状态驱动的 TUI 更加方便
blessed-vue: 类似于 blessed-react,但面向 Vue.js 开发者。它允许你使用 Vue 的模板语法和组件化思想来开发 TUI
调试 blessed 应用可能比调试传统的 Node.js 应用要棘手一些,因为程序的输出被 blessed 的界面占用了。以下是一些有效的调试方法:
使用日志文件:
这是最简单直接的方法。不要使用 console.log(因为它会扰乱 blessed 的屏幕渲染),而是将调试信息写入到一个单独的日志文件中。
const fs = require("fs")
function debugLog(message) {
fs.appendFileSync("debug.log", `[${new Date().toISOString()}] ${message}\n`)
}
// 在你的代码中
debugLog("Something happened")使用 blessed.log 组件:
在你的界面中专门开辟一个 Log 组件用于调试。你可以将需要观察的变量或事件信息实时输出到这个组件中。
Node.js 内置调试器:
你可以使用 Node.js 的内置调试器。通过 node inspect your-app.js 启动应用。然后你可以在另一个终端或 Chrome DevTools 中连接到调试服务。但这对于 TUI 来说可能体验不佳,因为断点会冻结整个界面。
VS Code 调试器:
配置 VS Code 的 launch.json 文件来进行调试是比较推荐的方式。你需要将调试控制台设置为一个外部或集成的终端,而不是默认的调试控制台。
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Blessed App",
"program": "${workspaceFolder}/your-app.js",
"console": "integratedTerminal" // 或 "externalTerminal"
}
]
}这样,你的 blessed 应用会运行在一个独立的终端窗口中,而你可以在 VS Code 中设置断点、检查变量。
这个例子演示组合使用 Listbar、List 和 Box 来创建一个简单的交互式应用
// layout-example.js
const blessed = require("blessed")
const screen = blessed.screen({
smartCSR: true,
title: "Component Layout Example"
})
// 顶部菜单栏
const menu = blessed.listbar({
parent: screen,
top: 0,
left: 0,
right: 0,
height: 1,
items: {
File: () => showContent("File Content"),
Edit: () => showContent("Edit Content"),
View: () => showContent("View Content"),
Help: () => showContent("Help Content"),
Exit: () => process.exit(0)
},
style: {
bg: "blue",
item: {
bg: "red",
hover: {
bg: "green"
}
},
selected: {
bg: "green"
}
}
})
// 内容显示区域
const contentBox = blessed.box({
parent: screen,
top: 1,
left: 0,
right: 0,
bottom: 0,
content: "Select an item from the menu...",
padding: 1,
style: {
bg: "#222"
}
})
function showContent(text) {
contentBox.setContent(text)
screen.render()
}
screen.key(["q", "C-c"], () => process.exit(0))
menu.focus()
screen.render()此示例展示使用 Log 组件实时显示日志,并使用 Prompt 与用户进行交互
// advanced-example.js
const blessed = require("blessed")
const screen = blessed.screen({ smartCSR: true, title: "Advanced Example" })
// 日志窗口
const logger = blessed.log({
parent: screen,
top: 0,
left: 0,
width: "70%",
height: "100%",
border: "line",
label: "Real-time Log",
scrollable: true,
alwaysScroll: true,
scrollbar: { ch: " ", track: { bg: "cyan" } },
keys: true,
vi: true
})
// 控制按钮
const sendButton = blessed.button({
parent: screen,
top: 1,
right: 1,
width: "25%",
height: 3,
content: "Send Command",
align: "center",
valign: "middle",
border: "line",
style: {
fg: "white",
bg: "green",
hover: { bg: "darkgreen" }
}
})
// 模拟日志输出
let logCounter = 0
setInterval(() => {
logger.log(`Log entry ${++logCounter} at ${new Date().toLocaleTimeString()}`)
}, 1000)
// 按钮点击事件
sendButton.on("press", () => {
const prompt = blessed.prompt({
parent: screen,
top: "center",
left: "center",
height: "shrink",
width: "shrink",
border: "line",
label: "Input Command"
})
prompt.input("Enter command:", "", (err, value) => {
if (value) {
logger.log(`> Command received: ${value}`)
}
screen.render()
})
})
screen.key(["q", "C-c"], () => process.exit(0))
screen.render()